#!/usr/bin/ruby -w

# Imports within Ruby's standard libraries
require 'openssl'
require 'net/http'

require 'json'

# Imports for the AppController
$:.unshift File.join(File.dirname(__FILE__), "..")
require 'djinn'

# Imports for AppController libraries
$:.unshift File.join(File.dirname(__FILE__))
require 'helperfunctions'

class InfrastructureManagerClient
  # The port that the InfrastructureManager runs on, by default.
  SERVER_PORT = 17444

  # A constant that indicates the number of second to wait.
  SMALL_WAIT = 3

  # The secret string that is used to authenticate this client with
  # InfrastructureManagers. It is initially generated by
  # appscale-run-instances and can be found on the machine that ran that tool,
  # or on any AppScale machine.
  attr_accessor :secret

  def initialize(secret, local_ip)
    @ip = local_ip
    @secret = secret
  end

  # Wrapper to call HTTP method.
  def make_call(request, uri)
    begin
      response = Net::HTTP.start(uri.hostname, uri.port) do |http|
        http.request(request)
        end
      if response.code != '200'
        log_message = "[IM] Error calling #{uri.hostname}:#{uri.port}#{uri.path}"
        log_message << ": #{response.message}" unless response.message.nil?
        Djinn.log_warn(log_message)
        raise AppScaleException.new(log_message)
      end
      return response.body
    rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ETIMEDOUT => error
      Djinn.log_warn(
        "[IM] Timeout when calling #{uri.hostname}:#{uri.port}#{uri.path}." \
        "Trying again. Error: #{error.message}")
      raise Djinn::FailedNodeException.new("Time out calling IaaS on " \
        "#{@ip}:#{SERVER_PORT}")
    rescue Errno::ECONNREFUSED => error
      Djinn.log_warn(
        "[IM] Connection refused when calling #{uri.hostname}:#{uri.port}" \
        "#{uri.path}. IaaS Manager may be down? Trying again. Error: " \
        "#{error.message}")
      raise Djinn::FailedNodeException.new("Connection refused calling IaaS on " \
        "#{@ip}:#{SERVER_PORT}")
    end
  end

  def describe_operation(operation_id)
    Djinn.log_debug('[IM] Calling describe_operation with id: ' +
      operation_id)
    uri = URI("http://#{@ip}:#{SERVER_PORT}/instances")
    headers = {}
    headers['Content-Type'] = 'application/json'
    headers['AppScale-Secret'] = @secret
    request = Net::HTTP::Get.new(uri.path, headers)

    request.body = JSON.dump({'operation_id' => operation_id})

    run_result = JSON.parse(make_call(request, uri))
    Djinn.log_debug("[IM] Operation #{operation_id} returned #{run_result}")
    return run_result
  end

  def terminate_instances(opts, instance_ids)
    headers = {}
    headers['Content-Type'] = 'application/json'
    headers['AppScale-Secret'] = @secret

    # Make a copy (the options are a simple hash so shallow copy does the
    # trick) to not modify the original.
    options = opts.clone
    options['instance_ids'] = [instance_ids] if instance_ids.class != Array

    uri = URI("http://#{@ip}:#{SERVER_PORT}/instances")

    request = Net::HTTP::Delete.new(uri.path, headers)

    request.body = JSON.dump(options)

    terminate_result = JSON.parse(make_call(request, uri))
    Djinn.log_debug("[IM] Terminate instances says [#{terminate_result}]")
    operation_id = terminate_result['operation_id']

    loop {
      begin
        describe_result = describe_operation(operation_id)
      rescue Djinn::FailedNodeException => error
        Djinn.log_warn(
          "[IM] Error describing terminate operation #{operation_id}. Error: " \
          "#{error.message}")
        next
      end
      Djinn.log_debug("[IM] Describe operation state is #{describe_result['state']}.")

      if describe_result['state'] == 'success'
        break
      elsif describe_result['state'] == 'failed'
        raise AppScaleException.new(describe_result['reason'])
      end
      Kernel.sleep(SMALL_WAIT)
    }
  end

  # Create new VMs.
  #
  # Args:
  #   num_vms: the number of VMs to create.
  #   opts: a hash containing information needed by the agent
  #     (credentials etc ...).
  #   roles: an Array containing the roles for each VM to be created.
  #   disks: an Array specifying the disks to be associated with the VMs
  #     (if any, it can be nil).
  #
  # Returns
  #   An Array containing the nodes information, suitable to be converted
  #   into Node.
  def run_instances(num_vms, opts, roles, disks)
    # Make a copy (the options are a simple hash so shallow copy does the
    # trick) to not modify the original.
    options = opts.clone
    options['num_vms'] = num_vms.to_s

    uri = URI("http://#{@ip}:#{SERVER_PORT}/instances")
    headers = {'Content-Type' => 'application/json',
               'AppScale-Secret' => @secret}
    request = Net::HTTP::Post.new(uri.path, headers)

    request.body = JSON.dump(options)

    run_result = JSON.parse(make_call(request, uri))
    Djinn.log_debug("[IM] Run instances info says [#{run_result}]")
    operation_id = run_result['operation_id']

    vm_info = {}
    loop {
      begin
        describe_result = describe_operation(operation_id)
      rescue Djinn::FailedNodeException => error
        Djinn.log_warn(
          "[IM] Error describing run instances operation #{operation_id}. " \
          "Error: #{error.message}")
        next
      end
      Djinn.log_debug("[IM] Describe run operation has vm_info " \
        "#{describe_result['vm_info'].inspect}.")

      if describe_result['state'] == 'success'
        vm_info = describe_result['vm_info']
        break
      elsif describe_result['state'] == 'failed'
        raise AppScaleException.new(describe_result['reason'])
      end
      Kernel.sleep(SMALL_WAIT)
    }

    # ip:role:instance-id
    instances_created = []
    vm_info['public_ips'].each_index { |index|
      tmp_roles = roles[index]
      tmp_roles = 'open' if roles[index].nil?
      instances_created << {
        'public_ip' => vm_info['public_ips'][index],
        'private_ip' => vm_info['private_ips'][index],
        'roles' => tmp_roles,
        'instance_id' => vm_info['instance_ids'][index],
        'disk' => disks[index],
        'instance_type' => options['instance_type']
      }
    }

    instances_created
  end

  # Asks the InfrastructureManager to attach a persistent disk to this machine.
  #
  # Args:
  #   opts: A Hash that contains the credentials necessary to interact
  #     with the underlying cloud infrastructure.
  #   disk_name: A String that names the persistent disk to attach to this
  #     machine.
  #   instance_id: A String that names this machine's instance id, needed to
  #     tell the InfrastructureManager which machine to attach the persistent
  #     disk to.
  # Returns:
  #   The location on the local filesystem where the persistent disk was
  #   attached to.
  def attach_disk(opts, disk_name, instance_id)
    Djinn.log_debug('Calling attach_disk with parameters ' \
      "#{opts.inspect}, with disk name #{disk_name} and instance id " +
      instance_id.to_s)

    # Make a copy (the options are a simple hash so shallow copy does the
    # trick) to not modify the original.
    options = opts.clone
    options['instance_id'] = instance_id
    options['disk_name'] = disk_name

    uri = URI("http://#{@ip}:#{SERVER_PORT}/instance")
    headers = {'Content-Type' => 'application/json',
               'AppScale-Secret' => @secret}
    request = Net::HTTP::Post.new(uri.path, headers)

    request.body = JSON.dump(options)

    return JSON.parse(make_call(request, uri))['location']
  end
end
